package com.wnxy.sddfp.system.service.impl;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.wnxy.sddfp.client.bank.TradeLogApi;
import com.wnxy.sddfp.common.constant.RedisConstant;
import com.wnxy.sddfp.common.constant.UserConstant;
import com.wnxy.sddfp.common.exception.Assert;
import com.wnxy.sddfp.common.exception.BusinessException;
import com.wnxy.sddfp.common.model.dto.bank.BankCardAddDTO;
import com.wnxy.sddfp.common.model.dto.bank.CardBindDTO;
import com.wnxy.sddfp.common.model.dto.bank.SignatureDTO;
import com.wnxy.sddfp.common.model.dto.user.LoginDto;
import com.wnxy.sddfp.common.model.dto.user.UserRegistDto;
import com.wnxy.sddfp.common.model.vo.user.LoginVo;
import com.wnxy.sddfp.common.model.vo.user.TokenVo;
import com.wnxy.sddfp.common.model.vo.user.UserInfoVo;
import com.wnxy.sddfp.common.result.Result;
import com.wnxy.sddfp.common.result.enums.impl.BusinessCode;
import com.wnxy.sddfp.common.util.IdGenerator;
import com.wnxy.sddfp.common.util.JwtTemplate;
import com.wnxy.sddfp.common.util.TokenUtils;
import com.wnxy.sddfp.system.entity.User;
import com.wnxy.sddfp.system.entity.UserCard;
import com.wnxy.sddfp.system.entity.UserDetail;
import com.wnxy.sddfp.system.mapper.UserCardMapper;
import com.wnxy.sddfp.system.mapper.UserDetailMapper;
import com.wnxy.sddfp.system.mapper.UserMapper;
import com.wnxy.sddfp.system.service.IUserDetailService;
import com.wnxy.sddfp.system.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.omg.CORBA.PRIVATE_MEMBER;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author Jet
 * @since 2023-04-26
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private IUserDetailService userDetailService;
    @Autowired
    private UserDetailMapper userDetailMapper;
    @Autowired
    private UserCardMapper userCardMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private JwtTemplate jwtTemplate;
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private TradeLogApi tradeLogApi;

    /**
     * isPhoneLogin为false，表示根据用户名密码登录
     * 实现：校验用户名密码是否存在
     * isPhoneLogin为true，表示根据手机号验证码登录
     * 实现：
     *      1、访问短信验证码接口，实现验证码发送 （sddfp-oss）
     *      2、校验验证码：获取redis中的验证码与用户输入的验证码对比
     *      3、手机号不存在自动注册功能：随机生成6位用户名、密码默认
     *      4、手机号存在则返回
     */
    @Override
    public LoginVo login(LoginDto loginDto) {
        return !loginDto.getIsPhoneLogin() ?
                loginByUP(loginDto.getLoginName(),loginDto.getLoginSecret()) :
                smsLogin(loginDto.getLoginName(),loginDto.getLoginSecret());
    }

    /**
     * 根据用户名密码登录
     * @param userName
     * @param password
     * @return
     */
    private LoginVo loginByUP(String userName, String password) {
        // 根据用户名查询，返回User对象
        User user = findByUserName(userName);
        // 校验用户名
        Assert.error(user == null, BusinessCode.USER_LOGIN_ERROR);
        // 校验密码
        Assert.error(
                !passwordEncoder.matches(password,user.getPwd()),
                BusinessCode.USER_LOGIN_ERROR
        );
        return getLoginVo(user);
    }

    private User findByUserName(String userName) {
        return this.getOne(Wrappers.<User>lambdaQuery().eq(User::getUserName,userName));
    }

    /**
     * 根据手机号验证码登录
     * @param phone
     * @param code
     * @return
     */
    private LoginVo smsLogin(String phone, String code) {
        //1. 校验验证码
        //1.1 先从redis中获取验证码
        BoundValueOperations<String, String> ops =
                stringRedisTemplate.boundValueOps(RedisConstant.SMS_PREFIX + phone);
        String redisCode = ops.get();
        //1.2 校验验证码-非空
        Assert.error(redisCode==null,BusinessCode.USER_VERIFY_CODE_ERROR);
        //1.3 校验验证码
        Assert.error(!redisCode.equals(code),BusinessCode.USER_VERIFY_CODE_ERROR);

        //2. 验证码校验通过，删除验证码
        stringRedisTemplate.delete(RedisConstant.SMS_PREFIX + phone);

        //3. 检查手机号是否存在，如果手机号不存在自动注册
        User user = this.findUserByPhone(phone);
        if (user == null) {
            UserRegistDto urDto = new UserRegistDto();
            urDto.setPhone(phone);
            urDto.setPwd("123456");
            urDto.setUserName(RandomUtil.randomString(6));
            // 自动注册
            this.saveUserInfo(urDto);

            // 手机号不存在，这里user为null，需要再查询一次
            user = this.findUserByPhone(phone);
        }
        return getLoginVo(user);
    }

    /**
     * 登录成功后返回结果
     * @param user
     * @return
     */
    private LoginVo getLoginVo(User user) {
        // 返回LoginVo对象
        LoginVo loginVo = BeanUtil.copyProperties(user,LoginVo.class);

        // 生成token
        Map<String,Object> payload = new HashMap<>();
        payload.put("id",user.getId().toString());
        payload.put("phone",user.getPhone());
        String token = jwtTemplate.createToken(payload);
        loginVo.setToken(token);
        return loginVo;
    }

    @Override
    public void regist(UserRegistDto userRegistDto) {
        // 根据手机号码查询
        Long count = checkPhoneExists(userRegistDto.getPhone());
        // 校验用户是否注册过
        Assert.error(count>0, BusinessCode.PHONE_EXISTED);
        // 保存用户、用户详情
        saveUserInfo(userRegistDto);
    }

    /**
     * 保存用户信息
     * 事务传播行为，控制事务边界范围。
     * propagation
     *   REQUIRED 当前运行方法必须有事务环境；如果没有则创建新的事务；如果当前方法已经有事务环境，则加入当前事务
     *            有则加入；没有就创建
     *   REQUIRES_NEW 不管有没有，都创建
     *   SUPPORTS 适应于查询方法；有事务就用事务，没有事务也可以运行
     */
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void saveUserInfo(UserRegistDto userRegistDto) {
        // 1、保存用户
        User user = BeanUtil.copyProperties(userRegistDto,User.class);
        // 密码加密
        user.setPwd(passwordEncoder.encode(user.getPwd()));
        user.setUserStatus(true);
        user.setUserType(UserConstant.USER_TYPE_PERSONAL);
        this.save(user);
        // 2、保存用户详情
        UserDetail userDetail = new UserDetail();
        userDetail.setId(user.getId());
        userDetail.setUserName(user.getUserName());
        userDetail.setPhone(user.getPhone());
        userDetail.setBorrowMoney(new BigDecimal("0"));
        userDetail.setUserCode(IdGenerator.generator(UserConstant.USER_CODE_PREFIX));
        userDetail.setCardBindStatus(false);
        userDetailService.save(userDetail);
    }

    /**
     * 根据手机号码查询是否存在
     * @param phone
     * @return 返回结果大于0，说明手机号码已经存在
     */
    private Long checkPhoneExists(String phone) {
        return baseMapper.selectCount(
                Wrappers.lambdaQuery(User.class).eq(User::getPhone,phone));
    }
    private User findUserByPhone(String phone) {
        return baseMapper.selectOne(
                Wrappers.lambdaQuery(User.class).eq(User::getPhone,phone));
    }


    @Override
    public Result addBankAccount(CardBindDTO cardBindDTO) {
        //1、从token中获取登录用户
        TokenVo tokenVo = TokenUtils.getTokenVo();
        //2、查询当前用户是否已经绑过卡: u_user_detail
        UserDetail userDetail = userDetailMapper.selectById(tokenVo.getId());
        Assert.error(userDetail.getCardBindStatus(),BusinessCode.USER_CARD_ALREADY_BINDED);

        //3、查询user_card表,如果卡号在表中已经存在且state=1,说明已经绑卡；直接返回失败
        UserCard userCard = userCardMapper.selectOne(
                Wrappers.lambdaQuery(UserCard.class)
                        .eq(UserCard::getCardNo, cardBindDTO.getCardNo()));
        Assert.error(userCard != null && userCard.getStatus(),BusinessCode.USER_CARD_ALREADY_BINDED);

        //4、用户没有绑卡，就保存用户绑卡数据到U_user_card表
        UserCard uc = new UserCard();
        uc.setUserId(Long.parseLong(tokenVo.getId()));
        uc.setPhone(tokenVo.getPhone());
        uc.setStatus(true);
        uc.setCardNo(cardBindDTO.getCardNo());
        // 保存绑卡记录
        userCardMapper.insert(uc);

        //5、请求参数中的真实姓名、身份证号，更新到user表中
        this.update(
                Wrappers.lambdaUpdate(User.class)
                        .set(User::getRealName,cardBindDTO.getRealName())
                        .set(User::getIdentityCode,cardBindDTO.getIdentityCode())
                        .eq(User::getId,uc.getUserId())
        );

        //6、生成请求流水号,远程访问托管代理服务。需要返回银行托管系统请求url数据。

        // 生成请求流水号
        String reqCode = IdGenerator.generator(
                UserConstant.REQUEST_CODE_PREFIX);
        // TODO 更新user_detail表设置reqCode; 因为绑卡成功消费MQ消息，需要根据流水号更改卡状态

        //RPC远程调用？ 1、RestTemplate；2、RibbitMQ RPC消息模型； 3、HttpUrlConnection；4、dubbo；5、SpringCloud
        //技术方案：通过RestTemplate实现远程调用。
        //String url = "http://localhost:5500/bank/access/card";
        //BankCardAddDTO bankCardAddDTO = new BankCardAddDTO();
        //bankCardAddDTO.setUserId(uc.getUserId());
        //bankCardAddDTO.setReqCode(reqCode);
        //SignatureDTO signatureDTO = restTemplate.postForObject(url, bankCardAddDTO, SignatureDTO.class);
        //return signatureDTO;

        /*
         * 通过SpringCloud OpenFeign 实现远程调用。
         * 解释：像调用本地方法一样调用远程方法。
         */
        BankCardAddDTO bankCardAddDTO = new BankCardAddDTO();
        bankCardAddDTO.setUserId(uc.getUserId());
        bankCardAddDTO.setReqCode(reqCode);
        Result result = tradeLogApi.proxyBindCardLog(bankCardAddDTO);
        return result;
    }

    @Override
    public UserInfoVo queryUserById(Long id) {
        User user = this.getById(id);

        if (user == null) {
            Assert.error(BusinessCode.USER_NOT_EXIST);
        }

        UserInfoVo userInfoVo = new UserInfoVo();
        userInfoVo.setIdentityCode(user.getIdentityCode());
        userInfoVo.setUserName(user.getUserName());

        UserDetail userDetail = userDetailMapper.selectById(id);
        if (userDetail != null) {
            userInfoVo.setUserCode(userDetail.getUserCode());
        }

        return userInfoVo;
    }

}
